package gui;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.GraphicsConfiguration;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.HeadlessException;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;

import logic.Square;
import logic.Sudoku;

import org.math.plot.Plot2DPanel;

import chromosomes.Chromosome;
import chromosomes.CrossoverFunction;
import chromosomes.GeneticAlgorithm;
import chromosomes.MutationFunction;
import chromosomes.TypeSelection;

/**
 * 
 * @author Marco Gallardo Casu
 * @author Miguel Cisneros Rojas
 * 
 */
public class GUI extends JFrame {

	/**
	 * 
	 */
	private static final long serialVersionUID = -9217225855867375781L;
	private static final Color BACKGROUND = new Color(0xEEEEEE);
	private static final Color RED = new Color(0xEE2255);
	private static final Color WHITE = new Color(0xFFFFFF);
	private static final String[] LABELS = { "Population", "Generations",
			"Crossover probability", "Mutation probability" };
	private static final String[] TEXT_FIELDS = { "100", "50", "0.7", "0.1" };
	private static final String[] EDITOR = { "Type selection:\n",
			"\nPopulation:\n", "\nGenerations:\n",
			"\nCrossover probability:\n", "\nCrossover function:\n",
			"\nMutation probability:\n", "\nMutation function:\n",
			"\nFitness:\n", "\nFitness function:\n" };
	private static final JComboBox<TypeSelection> cbTypeSelection = new JComboBox<TypeSelection>(
			TypeSelection.values());
	private static final JComboBox<CrossoverFunction> cbCrossoverFunction = new JComboBox<CrossoverFunction>(
			CrossoverFunction.values());
	private static final JComboBox<MutationFunction> cbMutationFunction = new JComboBox<MutationFunction>(
			MutationFunction.values());

	private static TypeSelection typeSelection = TypeSelection.RANKING;
	private static MutationFunction mutationFunction = MutationFunction.MUTATION_FUNCTION_1;
	private static CrossoverFunction crossoverFunction = CrossoverFunction.CROSSOVER_FUNCTION_1;
	private static boolean elitism = false;
	private static MyCanvas canvas = null;
	private static JTextField[] textFields = new JTextField[TEXT_FIELDS.length];
	private static Plot2DPanel graph;
	private static JTextPane editor;
	private static boolean[][] blocked;

	private int population;
	private int generations;
	private double crossover;
	private double mutation;
	private boolean resolved;
	private Sudoku loaded;
	private JFrame frame;
	private GeneticAlgorithm gA;

	public GUI() throws HeadlessException {
		super("Sudoku");
		frame = this;
		createSudoku("Sudoku", null, null, null, null);
		population = 100;
		generations = 50;
		crossover = 0.7;
		mutation = 0.1;
		resolved = false;
		loaded = null;
	}

	public GUI(GraphicsConfiguration arg0) {
		super("Sudoku", arg0);
		frame = this;
		createSudoku("Sudoku", null, null, null, null);
		population = 100;
		generations = 50;
		crossover = 0.7;
		mutation = 0.1;
		resolved = false;
		loaded = null;
	}

	public GUI(String arg0) throws HeadlessException {
		super(arg0);
		frame = this;
		createSudoku(arg0, null, null, null, null);
		population = 100;
		generations = 50;
		crossover = 0.7;
		mutation = 0.1;
		resolved = false;
		loaded = null;
	}

	public GUI(String arg0, GraphicsConfiguration arg1) {
		super(arg0, arg1);
		frame = this;
		createSudoku(arg0, null, null, null, null);
		population = 100;
		generations = 50;
		crossover = 0.7;
		mutation = 0.1;
		resolved = false;
		loaded = null;
	}

	public JMenuBar getJMenuBar() {
		JMenuBar bar = new JMenuBar();

		JMenu file = new JMenu("File");
		JMenuItem newFile = new JMenuItem("New file");
		JMenuItem openFile = new JMenuItem("Open file");
		JMenuItem saveFile = new JMenuItem("Save file");
		JMenuItem exit = new JMenuItem("Exit");
		JMenu options = new JMenu("Options");
		JCheckBoxMenuItem elitism = new JCheckBoxMenuItem("Elitism");
		elitism.setSelected(GUI.elitism);

		bar.add(file);
		file.add(newFile);
		file.addSeparator();
		file.add(openFile);
		file.add(saveFile);
		file.addSeparator();
		file.add(exit);
		bar.add(options);
		options.add(elitism);

		newFile.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent arg0) {
				createSudoku("Sudoku", null, null, null, null);
				resolved = false;
				loaded = null;
			}

		});

		openFile.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {
				try {
					open();
					resolved = false;
					loaded = (Sudoku) canvas.getSudoku().clone();
				} catch (ClassNotFoundException e1) {
					System.err.println(e1.getMessage());
				} catch (FileNotFoundException e2) {
					System.err.println(e2.getMessage());
				} catch (IOException e3) {
					System.err.println(e3.getMessage());
				}
			}

		});

		saveFile.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {
				try {
					save();
					resolved = false;
					loaded = (Sudoku) canvas.getSudoku().clone();
				} catch (FileNotFoundException e1) {
					System.err.println(e1.getMessage());
				} catch (IOException e2) {
					System.err.println(e2.getMessage());
				}
			}

		});

		exit.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent arg0) {
				loaded = null;
				System.exit(0);
			}

		});

		elitism.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {
				GUI.elitism = ((JCheckBoxMenuItem) e.getSource()).isSelected();
			}

		});

		return bar;
	}

	private JTabbedPane getJTabbedPane(String title, Sudoku sudoku,
			Chromosome c, boolean[][] blocked, boolean[][] bad) {
		JTabbedPane tabbedPane = new JTabbedPane();
		tabbedPane
				.addTab("Sudoku", getPanelSudoku(title, sudoku, blocked, bad));
		tabbedPane.addTab("Graph", getPanelGraph());
		tabbedPane.addTab("Statistics", getPanelStatistics(c));
		return tabbedPane;
	}

	private JPanel getPanelSudoku(String title, Sudoku sudoku,
			boolean[][] blocked, boolean[][] bad) {
		JPanel panel = new JPanel(new BorderLayout());
		if (sudoku == null) {
			canvas = new MyCanvas();
		} else {
			canvas = new MyCanvas(title, sudoku, blocked, bad);
		}
		panel.setBorder(BorderFactory.createLoweredBevelBorder());
		panel.add(canvas, BorderLayout.CENTER);
		panel.add(getPanelButtons(), BorderLayout.SOUTH);
		return panel;
	}

	private JPanel getPanelButtons() {
		JPanel panel = new JPanel(new GridBagLayout());
		JLabel[] labels = new JLabel[LABELS.length];

		GridBagConstraints constraints = new GridBagConstraints();
		constraints.gridx = 2;
		constraints.gridy = 0;
		constraints.anchor = GridBagConstraints.EAST;
		constraints.fill = GridBagConstraints.HORIZONTAL;
		panel.add(cbTypeSelection, constraints);

		constraints = new GridBagConstraints();
		constraints.gridx = 2;
		constraints.gridy = 1;
		constraints.anchor = GridBagConstraints.EAST;
		constraints.fill = GridBagConstraints.HORIZONTAL;
		constraints.gridheight = 1;
		panel.add(cbCrossoverFunction, constraints);

		constraints = new GridBagConstraints();
		constraints.gridx = 2;
		constraints.gridy = 2;
		constraints.anchor = GridBagConstraints.EAST;
		constraints.fill = GridBagConstraints.HORIZONTAL;
		constraints.gridheight = 1;
		panel.add(cbMutationFunction, constraints);

		for (int index = 0; index < LABELS.length; index++) {
			constraints = new GridBagConstraints();
			constraints.gridx = 0;
			constraints.gridy = index;
			constraints.anchor = GridBagConstraints.WEST;
			constraints.fill = GridBagConstraints.HORIZONTAL;
			constraints.insets = new Insets(2, 5, 4, 5);
			labels[index] = new JLabel(LABELS[index]);
			panel.add(labels[index], constraints);

			constraints = new GridBagConstraints();
			constraints.gridx = 1;
			constraints.gridy = index;
			constraints.anchor = GridBagConstraints.CENTER;
			constraints.fill = GridBagConstraints.HORIZONTAL;
			constraints.insets = new Insets(2, 5, 4, 5);
			constraints.ipadx = 50;
			textFields[index] = new JTextField(TEXT_FIELDS[index]);
			panel.add(textFields[index], constraints);

			final int i = index;
			textFields[index].addFocusListener(new FocusListener() {

				@Override
				public void focusLost(FocusEvent arg0) {
					String s = textFields[i].getText();
					if (s.isEmpty()) {
						textFields[i].setBackground(WHITE);
					} else {
						if (i == 0 || i == 1) {
							if (isInteger(s) && Integer.valueOf(s) >= 0) {
								textFields[i].setBackground(WHITE);
							} else {
								textFields[i].setBackground(RED);
							}
						} else {
							if (isDouble(s) && Double.valueOf(s) >= 0) {
								textFields[i].setBackground(WHITE);
							} else {
								textFields[i].setBackground(RED);
							}
						}
					}
				}

				@Override
				public void focusGained(FocusEvent arg0) {
					textFields[i].setBackground(BACKGROUND);
				}

			});

		}

		constraints = new GridBagConstraints();
		constraints.gridx = 2;
		constraints.gridy = LABELS.length - 1;
		constraints.anchor = GridBagConstraints.EAST;
		constraints.fill = GridBagConstraints.HORIZONTAL;
		JButton button = new JButton("Resolve");
		panel.add(button, constraints);

		cbTypeSelection.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent arg0) {
				typeSelection = (TypeSelection) ((JComboBox<?>) arg0
						.getSource()).getSelectedItem();
			}

		});

		cbCrossoverFunction.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent arg0) {
				crossoverFunction = (CrossoverFunction) ((JComboBox<?>) arg0
						.getSource()).getSelectedItem();
			}

		});

		cbMutationFunction.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent arg0) {
				mutationFunction = (MutationFunction) ((JComboBox<?>) arg0
						.getSource()).getSelectedItem();
			}

		});

		button.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent arg0) {
				resolve();
			}

		});

		return panel;
	}

	private Plot2DPanel getPanelGraph() {
		Plot2DPanel panel = new Plot2DPanel();
		panel.addLegend(Plot2DPanel.SOUTH);
		panel.setBorder(BorderFactory.createLoweredBevelBorder());
		return graph = panel;
	}

	private JPanel getPanelStatistics(Chromosome c) {
		JPanel panel = new JPanel(new BorderLayout());

		if (c == null) {
			editor = getTextEditor(EDITOR[0], "", EDITOR[1], "", EDITOR[2], "",
					EDITOR[3], "", EDITOR[4], "", EDITOR[5], "", EDITOR[6], "",
					EDITOR[7], "", EDITOR[8], "");
		} else {
			editor = getTextEditor(EDITOR[0], typeSelection.toString(),
					EDITOR[1], textFields[0].getText(), EDITOR[2],
					textFields[1].getText(), EDITOR[3],
					textFields[2].getText(), EDITOR[4],
					crossoverFunction.toString(), EDITOR[5],
					textFields[3].getText(), EDITOR[6],
					mutationFunction.toString(), EDITOR[7],
					String.valueOf(c.getFitness()), EDITOR[8], "");
		}

		editor.setBorder(BorderFactory.createLoweredBevelBorder());
		panel.add(editor, BorderLayout.NORTH);

		return panel;
	}

	private JTextPane getTextEditor(String... ss) {

		JTextPane editor = new JTextPane();

		try {
			SimpleAttributeSet attrs = new SimpleAttributeSet();
			StyleConstants.setBold(attrs, true);

			boolean attr = true;
			for (String s : ss) {
				if (attr) {
					editor.getStyledDocument().insertString(
							editor.getStyledDocument().getLength(), s, attrs);
				} else {
					editor.getStyledDocument().insertString(
							editor.getStyledDocument().getLength(), s, null);
				}
				attr = !attr;
			}

		} catch (BadLocationException e) {
			System.err.println(e.getMessage());
		}

		return editor;
	}

	private int getPopulation() {
		return Integer.valueOf(textFields[0].getText());
	}

	private int getGenerations() {
		return Integer.valueOf(textFields[1].getText());
	}

	private double getCrossover() {
		return Double.valueOf(textFields[2].getText());
	}

	private double getMutation() {
		return Double.valueOf(textFields[3].getText());
	}

	private void createSudoku(String title, Sudoku sudoku, Chromosome c,
			boolean[][] blocked, boolean[][] bad) {
		frame.dispose();
		frame = new JFrame(title);
		frame.setJMenuBar(getJMenuBar());
		frame.setContentPane(getJTabbedPane(title, sudoku, c, blocked, bad));
		frame.setSize(canvas.getWidth(), canvas.getHeight() + 150);
		frame.setLocationRelativeTo(null);
		frame.setEnabled(true);
		frame.setResizable(false);
		frame.setDefaultCloseOperation(EXIT_ON_CLOSE);
		frame.setVisible(true);
	}

	private void open() throws ClassNotFoundException, FileNotFoundException,
			IOException {
		JFileChooser fileChooser = new JFileChooser();
		FileNameExtensionFilter filter = new FileNameExtensionFilter(
				"Files .bin", "bin");
		fileChooser.setFileFilter(filter);
		int selection = fileChooser.showOpenDialog(GUI.this);
		File file = fileChooser.getSelectedFile();
		if ((selection == JFileChooser.APPROVE_OPTION)
				&& file.getName().endsWith(".bin")) {
			ObjectInputStream input = new ObjectInputStream(
					new FileInputStream(file));
			String name = file.getName();
			createSudoku(name.substring(0, name.length() - 4),
					(Sudoku) input.readObject(), null, null, null);
			textFields[0].setText(String.valueOf(population));
			textFields[1].setText(String.valueOf(generations));
			textFields[2].setText(String.valueOf(crossover));
			textFields[3].setText(String.valueOf(mutation));
			input.close();
		}
	}

	private void save() throws FileNotFoundException, IOException {
		JFileChooser fileChooser = new JFileChooser();
		FileNameExtensionFilter filter = new FileNameExtensionFilter(
				"Files .bin", "bin");
		fileChooser.setFileFilter(filter);
		int selection = fileChooser.showSaveDialog(GUI.this);
		if (selection == JFileChooser.APPROVE_OPTION) {
			File file = fileChooser.getSelectedFile();
			String root = file.getPath();
			if (!root.endsWith(".bin")) {
				root += ".bin";
				file = new File(root);
			}
			file = new File(root);
			file.createNewFile();
			if (file.canWrite()) {
				ObjectOutputStream output = new ObjectOutputStream(
						new FileOutputStream(file));
				output.writeObject(canvas.getSudoku());
				String name = file.getName();
				createSudoku(name.substring(0, name.length() - 4),
						canvas.getSudoku(), null, null, null);
				output.close();
			}
		}
	}

	private int[][] getGenes() {
		final int LENGTH = Chromosome.LENGTH;
		int[][] genes = new int[LENGTH][LENGTH];
		Sudoku sudoku = canvas.getSudoku();
		for (int square = 0; square < LENGTH; square++) {
			Square s = sudoku.getSquare(square);
			for (int row = 0; row < Square.LENGTH; row++) {
				for (int col = 0; col < Square.LENGTH; col++) {
					genes[square][row * Square.LENGTH + col] = s.getBox(row,
							col);
				}
			}
		}
		return genes;
	}

	private void resolve() {
		final int GENERATIONS = getGenerations();
		if (isCorrect() && getGenerations() > 0) {

			if (resolved && loaded != null) {
				canvas.setSudoku((Sudoku) loaded.clone());
			}

			population = getPopulation();
			generations = getGenerations();
			crossover = getCrossover();
			mutation = getMutation();
			int[][] genes = getGenes();

			if (!resolved) {
				final int LENGTH = Chromosome.LENGTH;
				blocked = new boolean[LENGTH][LENGTH];
				for (int square = 0; square < LENGTH; square++) {
					for (int box = 0; box < LENGTH; box++) {
						blocked[square][box] = genes[square][box] != 0;
					}
				}
			}

			gA = new GeneticAlgorithm(population, crossover, mutation,
					typeSelection, genes, elitism, crossoverFunction,
					mutationFunction, blocked);

			double[] average = new double[GENERATIONS];
			double[] bestGen = new double[GENERATIONS];
			double[] bestGlob = new double[GENERATIONS];

			long now = System.currentTimeMillis();
			gA.cycle();
			bestGen[0] = gA.getBestFitness();
			average[0] = gA.getAverageGen();
			bestGlob[0] = gA.getBestFitness();
			Chromosome best = gA.getBest();

			for (int i = 1; i < getGenerations(); i++) {
				gA.cycle();
				bestGen[i] = gA.getBestFitness();
				average[i] = gA.getAverageGen();
				if (bestGen[i] > bestGlob[i - 1]) {
					bestGlob[i] = bestGen[i];
					best = gA.getBest();
				} else {
					bestGlob[i] = bestGlob[i - 1];
				}
				/*
				 * System.out.println("Generation: " + i + "\tFitness: " +
				 * best.getFitness());
				 */
				System.out.print("Gen: " + i + "\tBest Glob: "
						+ best.getFitness());
				System.out.print("\tBest Gen: " + bestGen[i]);
				System.out.println("\t\tAverage: " + average[i]);
			}

			System.out.println("\nTiempo = "
					+ (System.currentTimeMillis() - (double) now) / 1000
					+ " s.");

			boolean[][] bad = new boolean[9][9];
			for (int square = 0; square < 9; square++) {
				for (int box = 0; box < 9; box++) {
					int[] squareMask = gA.getSquareMask(best, square);
					int[] rowMask = gA.getRowMask(best, square, box);
					int[] colMask = gA.getColMask(best, square, box);
					int gene = best.getGene(square, box);
					if (gene != 0) {
						if (squareMask[gene - 1] > 1 || rowMask[gene - 1] > 1
								|| colMask[gene - 1] > 1) {
							bad[square][box] = true;
						}
					}
				}
			}

			createSudoku(getTitle(), best.getSudoku(), best, blocked, bad);
			textFields[0].setText(String.valueOf(population));
			textFields[1].setText(String.valueOf(generations));
			textFields[2].setText(String.valueOf(crossover));
			textFields[3].setText(String.valueOf(mutation));
			resolved = true;
			graph.removeAllPlots();
			graph.addLinePlot("Generational average", average);
			graph.addLinePlot("Generational  best", bestGen);
			graph.addLinePlot("Global best", bestGlob);
		}
	}

	private boolean isInteger(String s) {
		try {
			Integer.valueOf(s);
			return true;
		} catch (NumberFormatException e) {
			return false;
		}
	}

	private boolean isDouble(String s) {
		try {
			Double.valueOf(s);
			return true;
		} catch (NumberFormatException e) {
			return false;
		}
	}

	private boolean isCorrect() {
		return isInteger(textFields[0].getText())
				&& isInteger(textFields[1].getText())
				&& isDouble(textFields[2].getText())
				&& isDouble(textFields[3].getText());
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		new GUI();
	}

}
